Skip to content

fix: unwrap() broken on wrapt 2.x due to ObjectProxy class identity mismatch#489

Open
thpierce wants to merge 2 commits intoaws:masterfrom
thpierce:fix/httplib-test-invalid-url
Open

fix: unwrap() broken on wrapt 2.x due to ObjectProxy class identity mismatch#489
thpierce wants to merge 2 commits intoaws:masterfrom
thpierce:fix/httplib-test-invalid-url

Conversation

@thpierce
Copy link
Contributor

@thpierce thpierce commented Mar 20, 2026

Problem

test_invalid_url in the httplib ext test suite fails with a TypeError on Python 3.8+ (where wrapt 2.x is installed). The test expects subsegment.cause to be a dict, but it gets a str (a cause ID reference to a nested subsegment).

The root cause is that unwrap() in aws_xray_sdk/ext/util.py is a silent no-op on wrapt 2.x, which causes wrapper accumulation across patch/unpatch cycles.

Root Cause

In wrapt 2.x, there are two different ObjectProxy classes:

  • wrapt.ObjectProxywrapt.proxies.ObjectProxy (pure Python, new in 2.x)
  • FunctionWrapper inherits from → _wrappers.ObjectProxy (C extension)

These are different class objects with different id()s. The unwrap() function checks:

if f and isinstance(f, wrapt.ObjectProxy) and hasattr(f, "__wrapped__"):

This isinstance() check always returns False for wrapt-wrapped functions on wrapt 2.x, so unwrap() never actually unwraps anything.

Impact

Since unwrap() is a no-op, every unpatch()patch() cycle adds another wrapper layer without removing the previous one:

  • In tests: The construct_ctx fixture calls patch()/unpatch() per test. By the 5th test (test_invalid_url), _send_request has 5 nested wrappers → 5 nested subsegments. Only the innermost gets the real exception dict; outer ones get string cause IDs → TypeError.
  • In production: Any application calling unpatch()/patch() (e.g. during reconfiguration) accumulates wrapper layers, causing subsegment bloat on every patched call.
  • Affects all ext modules that use unwrap(), not just httplib.

Why exactly 5 subsegments (not infinite)

Not recursion — just test position. test_invalid_url is the 5th test function in the file. Each prior test adds one wrapper layer via the fixture's patch/unpatch cycle.

Why it doesn't reproduce on macOS

The wrapt 2.x C extension wheel may not be available or may have a different class hierarchy on macOS, causing the pure-Python fallback to be used where the classes may be unified. On Linux (including GitHub Actions Ubuntu runners), the C extension .so is always present.

Fix

Remove the isinstance(f, wrapt.ObjectProxy) check from unwrap(). The hasattr(f, "__wrapped__") check is sufficient to identify wrapt-wrapped functions and works across all wrapt versions:

def unwrap(obj, attr):
    f = getattr(obj, attr, None)
    if f and hasattr(f, "__wrapped__"):
        setattr(obj, attr, f.__wrapped__)

Verification

Confirmed locally on Linux with wrapt 2.1.2 + Python 3.12:

  1. Before fix: 5 patch/unpatch cycles → wrapper depth 5 → 5 nested subsegments
  2. After fix: 5 patch/unpatch cycles → wrapper depth stays at 1 → 1 subsegment with correct cause=dict
  3. test_invalid_url passes with original test code (no workaround needed)

Changes

  • aws_xray_sdk/ext/util.py: Remove broken isinstance(f, wrapt.ObjectProxy) guard from unwrap()
  • tests/ext/httplib/test_httplib.py: No changes needed (original test code works with the fix)

Ticket: apm-telegen-2729

@thpierce thpierce requested a review from a team as a code owner March 20, 2026 16:35
@thpierce thpierce force-pushed the fix/httplib-test-invalid-url branch 6 times, most recently from 3528f35 to 6769f3b Compare March 20, 2026 18:15
…ismatch

In wrapt 2.x, wrapt.ObjectProxy points to wrapt.proxies.ObjectProxy (pure
Python) while FunctionWrapper inherits from a different ObjectProxy in the
C extension (_wrappers.so). This causes isinstance(f, wrapt.ObjectProxy)
to return False, making unwrap() a silent no-op.

Since unwrap() never actually unwraps, each patch/unpatch cycle in the test
fixture accumulates another wrapper layer. By the 5th test (test_invalid_url),
there are 5 nested wrappers creating 5 nested subsegments — only the
innermost gets the real exception dict, outer ones get string cause IDs.

Fix: remove the isinstance check from unwrap() — hasattr(f, '__wrapped__')
is sufficient to identify wrapt-wrapped functions.

This also fixes a user-facing bug: any application calling unpatch()/patch()
(e.g. during reconfiguration) would accumulate wrapper layers, causing
subsegment bloat on every patched call. Affects all ext modules using
unwrap(), not just httplib.
@thpierce thpierce force-pushed the fix/httplib-test-invalid-url branch from 6769f3b to 4115cbb Compare March 20, 2026 21:56
@thpierce thpierce changed the title fix: handle cause ID reference in test_invalid_url httplib test fix: unwrap() broken on wrapt 2.x due to ObjectProxy class identity mismatch Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants